diff --git a/changelogs/unreleased/gh-8485-vy-tuple-stat.md b/changelogs/unreleased/gh-8485-vy-tuple-stat.md
new file mode 100644
index 0000000000000000000000000000000000000000..eeaff6c89a28786358dbf1d4ca02f3c7af307b62
--- /dev/null
+++ b/changelogs/unreleased/gh-8485-vy-tuple-stat.md
@@ -0,0 +1,5 @@
+## feature/vinyl
+
+* Introduced the `memory.tuple` statistic for `box.stat.vinyl()` that shows
+  the total size of memory occupied by all tuples allocated by the Vinyl engine
+  (gh-8485).
diff --git a/src/box/vinyl.c b/src/box/vinyl.c
index 2cb41a222d8d5e87b8bc93e393f57ea9e6253e46..fc3201042d3a94c26b01f4f1910bdc7b3a9295c4 100644
--- a/src/box/vinyl.c
+++ b/src/box/vinyl.c
@@ -296,6 +296,7 @@ vy_info_append_memory(struct vy_env *env, struct info_handler *h)
 	info_table_begin(h, "memory");
 	info_append_int(h, "tx", vy_tx_manager_mem_used(env->xm));
 	info_append_int(h, "level0", lsregion_used(&env->mem_env.allocator));
+	info_append_int(h, "tuple", env->stmt_env.sum_tuple_size);
 	info_append_int(h, "tuple_cache", env->cache_env.mem_used);
 	info_append_int(h, "page_index", env->lsm_env.page_index_size);
 	info_append_int(h, "bloom_filter", env->lsm_env.bloom_size);
diff --git a/src/box/vy_stmt.c b/src/box/vy_stmt.c
index 4105b82d0362bcb426ab26efe564cfbb2afcd5b5..a6659082b4c25eddbea2680a0d231d0a7c1f3e67 100644
--- a/src/box/vy_stmt.c
+++ b/src/box/vy_stmt.c
@@ -98,6 +98,8 @@ vy_tuple_new(struct tuple_format *format, const char *data, const char *end)
 static void
 vy_tuple_delete(struct tuple_format *format, struct tuple *tuple)
 {
+	struct vy_stmt_env *env = format->engine;
+	size_t size = tuple_size(tuple);
 	say_debug("%s(%p)", __func__, tuple);
 	assert(tuple_is_unreferenced(tuple));
 	/*
@@ -105,10 +107,15 @@ vy_tuple_delete(struct tuple_format *format, struct tuple *tuple)
 	 * multithread unsafe modifications of a reference
 	 * counter.
 	 */
-	if (cord_is_main())
+	if (cord_is_main()) {
+		if (format != env->key_format) {
+			assert(env->sum_tuple_size >= size);
+			env->sum_tuple_size -= size;
+		}
 		tuple_format_unref(format);
+	}
 #ifndef NDEBUG
-	memset(tuple, '#', tuple_size(tuple)); /* fail early */
+	memset(tuple, '#', size); /* fail early */
 #endif
 	free(tuple);
 }
@@ -119,6 +126,7 @@ vy_stmt_env_create(struct vy_stmt_env *env)
 	env->tuple_format_vtab.tuple_new = vy_tuple_new;
 	env->tuple_format_vtab.tuple_delete = vy_tuple_delete;
 	env->max_tuple_size = 1024 * 1024;
+	env->sum_tuple_size = 0;
 	env->key_format = vy_simple_stmt_format_new(env, NULL, 0);
 	if (env->key_format == NULL)
 		panic("failed to create vinyl key format");
@@ -185,8 +193,11 @@ vy_stmt_alloc(struct tuple_format *format, uint32_t data_offset, uint32_t bsize)
 		  format->id, data_offset, bsize, tuple);
 	tuple_create(tuple, 1, tuple_format_id(format),
 		     data_offset, bsize, false);
-	if (cord_is_main())
+	if (cord_is_main()) {
+		if (format != env->key_format)
+			env->sum_tuple_size += total_size;
 		tuple_format_ref(format);
+	}
 	vy_stmt_set_lsn(tuple, 0);
 	vy_stmt_set_type(tuple, 0);
 	vy_stmt_set_flags(tuple, 0);
diff --git a/src/box/vy_stmt.h b/src/box/vy_stmt.h
index 3938c5a2af7066da034f543e8343c4c461616c80..771f26bbce0496f27eee78182783fd836a8cede4 100644
--- a/src/box/vy_stmt.h
+++ b/src/box/vy_stmt.h
@@ -74,6 +74,13 @@ struct vy_stmt_env {
 	 * @see box.cfg.vinyl_max_tuple_size
 	 */
 	size_t max_tuple_size;
+	/**
+	 * Size of memory occupied by all vinyl tuples allocated
+	 * in the main thread. Note, this doesn't include keys,
+	 * which should be fine because keys shouldn't stay in
+	 * memory for long.
+	 */
+	size_t sum_tuple_size;
 	/**
 	 * Tuple format used for creating key statements (e.g.
 	 * statements read from secondary index runs). It doesn't
diff --git a/test/vinyl-luatest/gh_8485_tuple_stat_test.lua b/test/vinyl-luatest/gh_8485_tuple_stat_test.lua
new file mode 100644
index 0000000000000000000000000000000000000000..4df1b7a22c0248728d43468f73148d0efaf4dcc6
--- /dev/null
+++ b/test/vinyl-luatest/gh_8485_tuple_stat_test.lua
@@ -0,0 +1,61 @@
+local server = require('luatest.server')
+local t = require('luatest')
+
+local g = t.group()
+
+g.before_all(function(cg)
+    cg.server = server:new()
+    cg.server:start()
+    cg.server:exec(function()
+        box.schema.create_space('test', {engine = 'vinyl'})
+        box.space.test:create_index('primary', {
+            parts = {1, 'unsigned'},
+        })
+        box.space.test:create_index('secondary', {
+            parts = {2, 'string'}, unique = false,
+        })
+        for i = 1, 10 do
+            box.space.test:insert({i, string.rep('x', 1000)})
+            if i == 5 then
+                box.snapshot()
+            end
+        end
+        collectgarbage()
+    end)
+end)
+
+g.after_all(function(cg)
+    cg.server:drop()
+end)
+
+g.test_tuple_stat = function(cg)
+    cg.server:exec(function()
+        local function gc()
+            box.tuple.new() -- drop blessed tuple ref
+            collectgarbage()
+        end
+        -- Initial value is 0.
+        gc()
+        t.assert_equals(box.stat.vinyl().memory.tuple, 0)
+
+        -- Tuples pinned by Lua.
+        box.cfg({vinyl_cache = 0})
+        local ret = box.space.test:select()
+        t.assert_equals(#ret, 10)
+        t.assert_almost_equals(box.stat.vinyl().memory.tuple, 10 * 1000, 1000)
+        ret = nil -- luacheck: ignore
+        gc()
+        t.assert_equals(box.stat.vinyl().memory.tuple, 0)
+
+        -- Tuples pinned by cache.
+        box.cfg({vinyl_cache = 100 * 1000})
+        ret = box.space.test:select()
+        t.assert_equals(#ret, 10)
+        t.assert_almost_equals(box.stat.vinyl().memory.tuple, 10 * 1000, 1000)
+        ret = nil -- luacheck: ignore
+        gc()
+        t.assert_almost_equals(box.stat.vinyl().memory.tuple, 10 * 1000, 1000)
+        box.cfg({vinyl_cache = 0})
+        t.assert_equals(box.stat.vinyl().memory.tuple, 0)
+    end)
+end
diff --git a/test/vinyl/stat.result b/test/vinyl/stat.result
index c4dc9af394e672a988d6e521213ca4b62ae3198c..909b6039c07274a0d5e85b51c2d945f36c48baaf 100644
--- a/test/vinyl/stat.result
+++ b/test/vinyl/stat.result
@@ -256,8 +256,9 @@ gstat()
     tuple_cache: 0
     tx: 0
     level0: 0
-    page_index: 0
     bloom_filter: 0
+    page_index: 0
+    tuple: 0
   disk:
     data_compacted: 0
     data: 0
@@ -1007,6 +1008,13 @@ stat_diff(gstat(), st, 'tx')
 ---
 - commit: 1
 ...
+-- free tuples pinned by Lua
+_ = nil
+---
+...
+_ = collectgarbage()
+---
+...
 -- box.stat.reset
 box.stat.reset()
 ---
@@ -1140,8 +1148,9 @@ gstat()
     tuple_cache: 14313
     tx: 0
     level0: 261562
-    page_index: 1250
     bloom_filter: 140
+    page_index: 1250
+    tuple: 13689
   disk:
     data_compacted: 104299
     data: 104299
diff --git a/test/vinyl/stat.test.lua b/test/vinyl/stat.test.lua
index a8657ccf4998149d7a4a1305a6ee249b6f3a991b..83c595509cd63e50885848b722e2200908f4d2ac 100644
--- a/test/vinyl/stat.test.lua
+++ b/test/vinyl/stat.test.lua
@@ -323,6 +323,10 @@ stat_diff(gstat(), st, 'tx')
 box.commit()
 stat_diff(gstat(), st, 'tx')
 
+-- free tuples pinned by Lua
+_ = nil
+_ = collectgarbage()
+
 -- box.stat.reset
 box.stat.reset()
 istat()