diff --git a/changelogs/unreleased/gh-8781-heap-use-after-free-in-memtx-tx-delete-gap.md b/changelogs/unreleased/gh-8781-heap-use-after-free-in-memtx-tx-delete-gap.md
new file mode 100644
index 0000000000000000000000000000000000000000..f3f7a0a0c296583842566ac034e25497d49904dc
--- /dev/null
+++ b/changelogs/unreleased/gh-8781-heap-use-after-free-in-memtx-tx-delete-gap.md
@@ -0,0 +1,5 @@
+## bugfix/memtx
+
+* Fixed a heap-use-after-free bug in the transaction manager, which could occur
+  when performing a DDL operation concurrently with a transaction on the same
+  space (gh-8781).
diff --git a/src/box/memtx_tx.c b/src/box/memtx_tx.c
index 852705eae5b1e3e65d9da013712c1c22e7f5f071..df34ef32d48842de42b2c1172a996d4791311d40 100644
--- a/src/box/memtx_tx.c
+++ b/src/box/memtx_tx.c
@@ -946,15 +946,18 @@ memtx_tx_story_new(struct space *space, struct tuple *tuple)
 	return story;
 }
 
+/**
+ * Deletes a story. Expects the story to be fully unlinked.
+ */
 static void
 memtx_tx_story_delete(struct memtx_story *story)
 {
-	/* Expecting to delete fully unlinked story. */
 	assert(story->add_stmt == NULL);
 	assert(story->del_stmt == NULL);
 	for (uint32_t i = 0; i < story->index_count; i++) {
 		assert(story->link[i].newer_story == NULL);
 		assert(story->link[i].older_story == NULL);
+		assert(rlist_empty(&story->link[i].read_gaps));
 	}
 
 	memtx_tx_stats_discard(&txm.story_stats[story->status],
@@ -2943,6 +2946,16 @@ memtx_tx_on_space_delete(struct space *space)
 		if (story->del_stmt != NULL)
 			memtx_tx_history_remove_stmt(story->del_stmt);
 		memtx_tx_story_full_unlink_on_space_delete(story);
+		for (uint32_t i = 0; i < story->index_count; i++) {
+			struct rlist *read_gaps = &story->link[i].read_gaps;
+			while (!rlist_empty(&story->link[i].read_gaps)) {
+				struct gap_item_base *item =
+					rlist_first_entry(read_gaps,
+							  struct gap_item_base,
+							  in_read_gaps);
+				memtx_tx_delete_gap(item);
+			}
+		}
 		memtx_tx_story_delete(story);
 	}
 }