diff --git a/lib/shared/dm-ops/dm-op-specs.js b/lib/shared/dm-ops/dm-op-specs.js
--- a/lib/shared/dm-ops/dm-op-specs.js
+++ b/lib/shared/dm-ops/dm-op-specs.js
@@ -5,6 +5,7 @@
 import { createThreadSpec } from './create-thread-spec.js';
 import type { DMOperationSpec } from './dm-op-spec.js';
 import { joinThreadSpec } from './join-thread-spec.js';
+import { leaveThreadSpec } from './leave-thread-spec.js';
 import { sendEditMessageSpec } from './send-edit-message-spec.js';
 import { sendReactionMessageSpec } from './send-reaction-message-spec.js';
 import { sendTextMessageSpec } from './send-text-message-spec.js';
@@ -20,4 +21,5 @@
   [dmOperationTypes.SEND_EDIT_MESSAGE]: sendEditMessageSpec,
   [dmOperationTypes.ADD_MEMBERS]: addMembersSpec,
   [dmOperationTypes.JOIN_THREAD]: joinThreadSpec,
+  [dmOperationTypes.LEAVE_THREAD]: leaveThreadSpec,
 });
diff --git a/lib/shared/dm-ops/leave-thread-spec.js b/lib/shared/dm-ops/leave-thread-spec.js
new file mode 100644
--- /dev/null
+++ b/lib/shared/dm-ops/leave-thread-spec.js
@@ -0,0 +1,76 @@
+// @flow
+
+import uuid from 'uuid';
+
+import type {
+  DMOperationSpec,
+  ProcessDMOperationUtilities,
+} from './dm-op-spec.js';
+import type { DMLeaveThreadOperation } from '../../types/dm-ops.js';
+import { messageTypes } from '../../types/message-types-enum.js';
+import type { ThickRawThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js';
+import { updateTypes } from '../../types/update-types-enum.js';
+import type { ClientUpdateInfo } from '../../types/update-types.js';
+import { userIsMember } from '../thread-utils.js';
+
+const leaveThreadSpec: DMOperationSpec<DMLeaveThreadOperation> = Object.freeze({
+  processDMOperation: async (
+    dmOperation: DMLeaveThreadOperation,
+    viewerID: string,
+    utilities: ProcessDMOperationUtilities,
+  ) => {
+    const { editorID, time, messageID, threadID } = dmOperation;
+
+    const threadInfoOptional = utilities.getThreadInfo(threadID);
+    if (!threadInfoOptional || !threadInfoOptional.thick) {
+      // We can't perform this operation now. It should be queued for later.
+      return {
+        rawMessageInfos: [],
+        updateInfos: [],
+      };
+    }
+    const threadInfo: ThickRawThreadInfo = threadInfoOptional;
+    if (!userIsMember(threadInfo, editorID)) {
+      return {
+        rawMessageInfos: [],
+        updateInfos: [],
+      };
+    }
+
+    const leaveThreadMessage = {
+      type: messageTypes.LEAVE_THREAD,
+      id: messageID,
+      threadID,
+      creatorID: editorID,
+      time,
+    };
+
+    const updateInfos: Array<ClientUpdateInfo> = [];
+    if (viewerID === editorID) {
+      updateInfos.push({
+        type: updateTypes.DELETE_THREAD,
+        id: uuid.v4(),
+        time,
+        threadID,
+      });
+    } else {
+      const updatedThreadInfo = {
+        ...threadInfo,
+        members: threadInfo.members.filter(member => member.id !== editorID),
+      };
+      updateInfos.push({
+        type: updateTypes.UPDATE_THREAD,
+        id: uuid.v4(),
+        time,
+        threadInfo: updatedThreadInfo,
+      });
+    }
+
+    return {
+      rawMessageInfos: [leaveThreadMessage],
+      updateInfos,
+    };
+  },
+});
+
+export { leaveThreadSpec };
diff --git a/lib/types/dm-ops.js b/lib/types/dm-ops.js
--- a/lib/types/dm-ops.js
+++ b/lib/types/dm-ops.js
@@ -29,6 +29,7 @@
   SEND_EDIT_MESSAGE: 'send_edit_message',
   ADD_MEMBERS: 'add_members',
   JOIN_THREAD: 'join_thread',
+  LEAVE_THREAD: 'leave_thread',
 });
 export type DMOperationType = $Values<typeof dmOperationTypes>;
 
@@ -186,6 +187,22 @@
     rawEntryInfos: t.list(rawEntryInfoValidator),
   });
 
+export type DMLeaveThreadOperation = {
+  +type: 'leave_thread',
+  +editorID: string,
+  +time: number,
+  +messageID: string,
+  +threadID: string,
+};
+export const dmLeaveThreadOperationValidator: TInterface<DMLeaveThreadOperation> =
+  tShape<DMLeaveThreadOperation>({
+    type: tString(dmOperationTypes.LEAVE_THREAD),
+    editorID: tUserID,
+    time: t.Number,
+    messageID: t.String,
+    threadID: t.String,
+  });
+
 export type DMOperation =
   | DMCreateThreadOperation
   | DMCreateSidebarOperation
@@ -193,7 +210,8 @@
   | DMSendReactionMessageOperation
   | DMSendEditMessageOperation
   | DMAddMembersOperation
-  | DMJoinThreadOperation;
+  | DMJoinThreadOperation
+  | DMLeaveThreadOperation;
 export const dmOperationValidator: TUnion<DMOperation> = t.union([
   dmCreateThreadOperationValidator,
   dmCreateSidebarOperationValidator,
@@ -202,6 +220,7 @@
   dmSendEditMessageOperationValidator,
   dmAddMembersOperationValidator,
   dmJoinThreadOperationValidator,
+  dmLeaveThreadOperationValidator,
 ]);
 
 export type DMOperationResult = {