diff --git a/src/DraftCleaner.ts b/src/DraftCleaner.ts
new file mode 100644
index 00000000000..5e6c1cbae7f
--- /dev/null
+++ b/src/DraftCleaner.ts
@@ -0,0 +1,78 @@
+/*
+Copyright 2024 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { logger } from "matrix-js-sdk/src/logger";
+
+import { MatrixClientPeg } from "./MatrixClientPeg";
+import { EDITOR_STATE_STORAGE_PREFIX } from "./components/views/rooms/SendMessageComposer";
+
+// The key used to persist the the timestamp we last cleaned up drafts
+export const DRAFT_LAST_CLEANUP_KEY = "mx_draft_cleanup";
+// The period of time we wait between cleaning drafts
+export const DRAFT_CLEANUP_PERIOD = 1000 * 60 * 60 * 24 * 30;
+
+/**
+ * Checks if `DRAFT_CLEANUP_PERIOD` has expired, if so, deletes any stord editor drafts that exist for rooms that are not in the known list.
+ */
+export function cleanUpDraftsIfRequired(): void {
+    if (!shouldCleanupDrafts()) {
+        return;
+    }
+    logger.debug(`Cleaning up editor drafts...`);
+    cleaupDrafts();
+    try {
+        localStorage.setItem(DRAFT_LAST_CLEANUP_KEY, String(Date.now()));
+    } catch (error) {
+        logger.error("Failed to persist draft cleanup key", error);
+    }
+}
+
+/**
+ *
+ * @returns {bool} True if the timestamp has not been persisted or the `DRAFT_CLEANUP_PERIOD` has expired.
+ */
+function shouldCleanupDrafts(): boolean {
+    try {
+        const lastCleanupTimestamp = localStorage.getItem(DRAFT_LAST_CLEANUP_KEY);
+        if (!lastCleanupTimestamp) {
+            return true;
+        }
+        const parsedTimestamp = Number.parseInt(lastCleanupTimestamp || "", 10);
+        if (!Number.isInteger(parsedTimestamp)) {
+            return true;
+        }
+        return Date.now() > parsedTimestamp + DRAFT_CLEANUP_PERIOD;
+    } catch (error) {
+        return true;
+    }
+}
+
+/**
+ * Clear all drafts for the CIDER editor if the room does not exist in the known rooms.
+ */
+function cleaupDrafts(): void {
+    for (let i = 0; i < localStorage.length; i++) {
+        const keyName = localStorage.key(i);
+        if (!keyName?.startsWith(EDITOR_STATE_STORAGE_PREFIX)) continue;
+        // Remove the prefix and the optional event id suffix to leave the room id
+        const roomId = keyName.slice(EDITOR_STATE_STORAGE_PREFIX.length).split("_$")[0];
+        const room = MatrixClientPeg.safeGet().getRoom(roomId);
+        if (!room) {
+            logger.debug(`Removing draft for unknown room with key ${keyName}`);
+            localStorage.removeItem(keyName);
+        }
+    }
+}
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index 8335b0e3f67..53514ab817b 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -143,6 +143,7 @@ import { checkSessionLockFree, getSessionLock } from "../../utils/SessionLock";
 import { SessionLockStolenView } from "./auth/SessionLockStolenView";
 import { ConfirmSessionLockTheftView } from "./auth/ConfirmSessionLockTheftView";
 import { LoginSplashView } from "./auth/LoginSplashView";
+import { cleanUpDraftsIfRequired } from "../../DraftCleaner";
 
 // legacy export
 export { default as Views } from "../../Views";
@@ -1528,6 +1529,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
             }
 
             if (state === SyncState.Syncing && prevState === SyncState.Syncing) {
+                // We know we have performabed a live update and known rooms should be in a good state.
+                // Now is a good time to clean up drafts.
+                cleanUpDraftsIfRequired();
                 return;
             }
             logger.debug(`MatrixClient sync state => ${state}`);
diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx
index eee2a476a7e..9e986a181ec 100644
--- a/src/components/views/rooms/SendMessageComposer.tsx
+++ b/src/components/views/rooms/SendMessageComposer.tsx
@@ -71,6 +71,9 @@ import { IDiff } from "../../../editor/diff";
 import { getBlobSafeMimeType } from "../../../utils/blobs";
 import { EMOJI_REGEX } from "../../../HtmlUtils";
 
+// The prefix used when persisting editor drafts to localstorage.
+export const EDITOR_STATE_STORAGE_PREFIX = "mx_cider_state_";
+
 /**
  * Build the mentions information based on the editor model (and any related events):
  *
@@ -604,7 +607,7 @@ export class SendMessageComposer extends React.Component<ISendMessageComposerPro
     }
 
     private get editorStateKey(): string {
-        let key = `mx_cider_state_${this.props.room.roomId}`;
+        let key = EDITOR_STATE_STORAGE_PREFIX + this.props.room.roomId;
         if (this.props.relation?.rel_type === THREAD_RELATION_TYPE.name) {
             key += `_${this.props.relation.event_id}`;
         }
diff --git a/test/components/structures/MatrixChat-test.tsx b/test/components/structures/MatrixChat-test.tsx
index d2d4178d1f8..f4c0fb7ffa2 100644
--- a/test/components/structures/MatrixChat-test.tsx
+++ b/test/components/structures/MatrixChat-test.tsx
@@ -62,6 +62,7 @@ import { SettingLevel } from "../../../src/settings/SettingLevel";
 import { MatrixClientPeg as peg } from "../../../src/MatrixClientPeg";
 import DMRoomMap from "../../../src/utils/DMRoomMap";
 import { ReleaseAnnouncementStore } from "../../../src/stores/ReleaseAnnouncementStore";
+import { DRAFT_LAST_CLEANUP_KEY } from "../../../src/DraftCleaner";
 
 jest.mock("matrix-js-sdk/src/oidc/authorize", () => ({
     completeAuthorizationCodeGrant: jest.fn(),
@@ -598,6 +599,41 @@ describe("<MatrixChat />", () => {
             expect(screen.getByText(`Welcome ${userId}`)).toBeInTheDocument();
         });
 
+        describe("clean up drafts", () => {
+            const roomId = "!room:server.org";
+            const unknownRoomId = "!room2:server.org";
+            const room = new Room(roomId, mockClient, userId);
+            const timestamp = 2345678901234;
+            beforeEach(() => {
+                localStorage.setItem(`mx_cider_state_${unknownRoomId}`, "fake_content");
+                localStorage.setItem(`mx_cider_state_${roomId}`, "fake_content");
+                mockClient.getRoom.mockImplementation((id) => [room].find((room) => room.roomId === id) || null);
+            });
+            afterEach(() => {
+                jest.restoreAllMocks();
+            });
+            it("should clean up drafts", async () => {
+                Date.now = jest.fn(() => timestamp);
+                localStorage.setItem(`mx_cider_state_${roomId}`, "fake_content");
+                localStorage.setItem(`mx_cider_state_${unknownRoomId}`, "fake_content");
+                await getComponentAndWaitForReady();
+                mockClient.emit(ClientEvent.Sync, SyncState.Syncing, SyncState.Syncing);
+                // let things settle
+                await flushPromises();
+                expect(localStorage.getItem(`mx_cider_state_${roomId}`)).not.toBeNull();
+                expect(localStorage.getItem(`mx_cider_state_${unknownRoomId}`)).toBeNull();
+            });
+
+            it("should not clean up drafts before expiry", async () => {
+                // Set the last cleanup to the recent past
+                localStorage.setItem(`mx_cider_state_${unknownRoomId}`, "fake_content");
+                localStorage.setItem(DRAFT_LAST_CLEANUP_KEY, String(timestamp - 100));
+                await getComponentAndWaitForReady();
+                mockClient.emit(ClientEvent.Sync, SyncState.Syncing, SyncState.Syncing);
+                expect(localStorage.getItem(`mx_cider_state_${unknownRoomId}`)).not.toBeNull();
+            });
+        });
+
         describe("onAction()", () => {
             beforeEach(() => {
                 jest.spyOn(defaultDispatcher, "dispatch").mockClear();