diff --git a/jest.config.js b/jest.config.js
index 73521d81e..c47992ee5 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -23,4 +23,6 @@ module.exports = {
     "!www/js/**/index.{ts,tsx,js,jsx}",
     "!www/js/types/**/*.{ts,tsx,js,jsx}",
   ],
+  // several functions in commHelper do not have unit tests; see note in commHelper.test.ts
+  coveragePathIgnorePatterns: ['www/js/services/commHelper.ts'],
 };
diff --git a/www/__tests__/commHelper.test.ts b/www/__tests__/commHelper.test.ts
index d7018abb5..c66fb5f36 100644
--- a/www/__tests__/commHelper.test.ts
+++ b/www/__tests__/commHelper.test.ts
@@ -47,4 +47,8 @@ it('fetches text from a URL and caches it so the next call is faster', async ()
   * - updateUser
   * - getUser
   * - putOne
+  * - getUserCustomLabels
+  * - insertUserCustomLabel
+  * - updateUserCustomLabel
+  * - deleteUserCustomLabel
 */
diff --git a/www/i18n/en.json b/www/i18n/en.json
index 7347f8e46..738df38f5 100644
--- a/www/i18n/en.json
+++ b/www/i18n/en.json
@@ -8,7 +8,10 @@
   "trip-confirm": {
     "services-please-fill-in": "Please fill in the {{text}} not listed.",
     "services-cancel": "Cancel",
-    "services-save": "Save"
+    "services-save": "Save",
+    "custom-mode": "Custom Mode",
+    "custom-purpose": "Custom Purpose",
+    "custom-labels": "Custom Labels"
   },
 
   "control": {
@@ -52,7 +55,8 @@
     "refresh-app-config": "Refresh App Configuration",
     "current-version": "Current version: {{version}}",
     "refreshing-app-config": "Refreshing app configuration, please wait...",
-    "already-up-to-date": "Already up to date!"
+    "already-up-to-date": "Already up to date!",
+    "manage-custom-labels": "Manage Custom Labels"
   },
 
   "general-settings": {
diff --git a/www/js/App.tsx b/www/js/App.tsx
index d59d7f270..0d45ebeca 100644
--- a/www/js/App.tsx
+++ b/www/js/App.tsx
@@ -13,16 +13,22 @@ import usePermissionStatus from './usePermissionStatus';
 import { initPushNotify } from './splash/pushNotifySettings';
 import { initStoreDeviceSettings } from './splash/storeDeviceSettings';
 import { initRemoteNotifyHandler } from './splash/remoteNotifyHandler';
+import { getUserCustomLabels } from './services/commHelper';
 import { initCustomDatasetHelper } from './metrics/customMetricsHelper';
 import AlertBar from './components/AlertBar';
 import Main from './Main';
 
 export const AppContext = createContext<any>({});
+const CUSTOM_LABEL_KEYS_IN_DATABASE = ['mode', 'purpose'];
+type CustomLabelMap = {
+  [k: string]: string[];
+};
 
 const App = () => {
   // will remain null while the onboarding state is still being determined
   const [onboardingState, setOnboardingState] = useState<OnboardingState | null>(null);
   const [permissionsPopupVis, setPermissionsPopupVis] = useState(false);
+  const [customLabelMap, setCustomLabelMap] = useState<CustomLabelMap>({});
   const appConfig = useAppConfig();
   const permissionStatus = usePermissionStatus();
 
@@ -39,6 +45,7 @@ const App = () => {
     initPushNotify();
     initStoreDeviceSettings();
     initRemoteNotifyHandler();
+    getUserCustomLabels(CUSTOM_LABEL_KEYS_IN_DATABASE).then((res) => setCustomLabelMap(res));
     initCustomDatasetHelper(appConfig);
   }, [appConfig]);
 
@@ -50,6 +57,8 @@ const App = () => {
     permissionStatus,
     permissionsPopupVis,
     setPermissionsPopupVis,
+    customLabelMap,
+    setCustomLabelMap,
   };
 
   let appContent;
diff --git a/www/js/control/CustomLabelSettingRow.tsx b/www/js/control/CustomLabelSettingRow.tsx
new file mode 100644
index 000000000..c106d0985
--- /dev/null
+++ b/www/js/control/CustomLabelSettingRow.tsx
@@ -0,0 +1,166 @@
+import React, { useState, useContext } from 'react';
+import SettingRow from './SettingRow';
+import {
+  Modal,
+  View,
+  Text,
+  TouchableOpacity,
+  StyleSheet,
+  useWindowDimensions,
+  ScrollView,
+} from 'react-native';
+import { Icon, TextInput, Dialog, Button, useTheme, SegmentedButtons } from 'react-native-paper';
+import { AppContext } from '../App';
+import { useTranslation } from 'react-i18next';
+import { deleteUserCustomLabel, insertUserCustomLabel } from '../services/commHelper';
+import { displayErrorMsg, logDebug } from '../plugin/logger';
+import { labelKeyToReadable, readableLabelToKey } from '../survey/multilabel/confirmHelper';
+
+const CustomLabelSettingRow = () => {
+  const [isCustomLabelModalOpen, setIsCustomLabelModalOpen] = useState(false);
+  const { customLabelMap, setCustomLabelMap } = useContext(AppContext);
+  const [isAddLabelOn, setIsAddLabelOn] = useState(false);
+  const [text, setText] = useState('');
+  const [key, setKey] = useState('mode');
+
+  const { t } = useTranslation(); //this accesses the translations
+  const { colors } = useTheme(); // use this to get the theme colors instead of hardcoded #hex colors
+  const { height } = useWindowDimensions();
+
+  const labelKeysButton = [
+    {
+      value: 'mode',
+      label: t('diary.mode'),
+    },
+    {
+      value: 'purpose',
+      label: t('diary.purpose'),
+    },
+  ];
+
+  const onDeleteLabel = async (label) => {
+    const processedLabel = readableLabelToKey(label);
+    try {
+      const res = await deleteUserCustomLabel(key, processedLabel);
+      if (res) {
+        setCustomLabelMap({
+          ...customLabelMap,
+          [key]: res['label'],
+        });
+        logDebug(`Successfuly deleted custom ${key}, ${JSON.stringify(res)}`);
+      }
+    } catch (e) {
+      displayErrorMsg(e, 'Delete Mode Error');
+    }
+  };
+
+  const onSaveLabel = async () => {
+    const processedLabel = readableLabelToKey(text);
+    if (customLabelMap[key]?.length > 0 && customLabelMap[key].indexOf(processedLabel) > -1) {
+      return;
+    }
+    try {
+      const res = await insertUserCustomLabel(key, processedLabel);
+      if (res) {
+        setText('');
+        setCustomLabelMap({
+          ...customLabelMap,
+          [key]: res['label'],
+        });
+        setIsAddLabelOn(false);
+        logDebug(`Successfuly inserted custom ${key}, ${JSON.stringify(res)}`);
+      }
+    } catch (e) {
+      displayErrorMsg(e, 'Create Mode Error');
+    }
+  };
+
+  return (
+    <>
+      <SettingRow
+        textKey="control.manage-custom-labels"
+        iconName="label-multiple"
+        action={() => setIsCustomLabelModalOpen(true)}></SettingRow>
+      <Modal
+        visible={isCustomLabelModalOpen}
+        onDismiss={() => setIsCustomLabelModalOpen(false)}
+        transparent={true}>
+        <Dialog visible={isCustomLabelModalOpen} onDismiss={() => setIsCustomLabelModalOpen(false)}>
+          <Dialog.Title>
+            <Text>{t('trip-confirm.custom-labels')}</Text>
+            <TouchableOpacity style={styles.plusIconWrapper} onPress={() => setIsAddLabelOn(true)}>
+              <Icon source="plus-circle" size={24} />
+            </TouchableOpacity>
+          </Dialog.Title>
+          <Dialog.Content>
+            <SegmentedButtons
+              style={{ marginBottom: 10 }}
+              value={key}
+              onValueChange={setKey}
+              buttons={labelKeysButton}
+            />
+            {isAddLabelOn && (
+              <>
+                <TextInput
+                  label={t('trip-confirm.services-please-fill-in', {
+                    text: key,
+                  })}
+                  value={text}
+                  onChangeText={setText}
+                  maxLength={25}
+                  style={{ marginTop: 10 }}
+                />
+                <View style={styles.saveButtonWrapper}>
+                  <Button onPress={() => setIsAddLabelOn(false)}>
+                    {t('trip-confirm.services-cancel')}
+                  </Button>
+                  <Button onPress={onSaveLabel}>{t('trip-confirm.services-save')}</Button>
+                </View>
+              </>
+            )}
+            <ScrollView contentContainerStyle={{ height: height / 2 }}>
+              {customLabelMap[key]?.length > 0 &&
+                customLabelMap[key].map((label, idx) => {
+                  return (
+                    <View
+                      key={label + idx}
+                      style={[styles.itemWrapper, { borderBottomColor: colors.outline }]}>
+                      <Text>{labelKeyToReadable(label)}</Text>
+                      <TouchableOpacity onPress={() => onDeleteLabel(label)}>
+                        <Icon source="trash-can" size={20} />
+                      </TouchableOpacity>
+                    </View>
+                  );
+                })}
+            </ScrollView>
+          </Dialog.Content>
+          <Dialog.Actions>
+            <Button onPress={() => setIsCustomLabelModalOpen(false)}>
+              {t('trip-confirm.services-cancel')}
+            </Button>
+          </Dialog.Actions>
+        </Dialog>
+      </Modal>
+    </>
+  );
+};
+
+const styles = StyleSheet.create({
+  itemWrapper: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    paddingVertical: 16,
+    borderBottomWidth: 1,
+  },
+  saveButtonWrapper: {
+    display: 'flex',
+    flexDirection: 'row',
+    justifyContent: 'flex-end',
+  },
+  plusIconWrapper: {
+    position: 'absolute',
+    right: 0,
+  },
+});
+
+export default CustomLabelSettingRow;
diff --git a/www/js/control/ProfileSettings.tsx b/www/js/control/ProfileSettings.tsx
index 264ba8ba0..ef61e0a24 100644
--- a/www/js/control/ProfileSettings.tsx
+++ b/www/js/control/ProfileSettings.tsx
@@ -37,6 +37,7 @@ import { storageClear } from '../plugin/storage';
 import { getAppVersion } from '../plugin/clientStats';
 import { getConsentDocument } from '../splash/startprefs';
 import { displayError, displayErrorMsg, logDebug, logWarn } from '../plugin/logger';
+import CustomLabelSettingRow from './CustomLabelSettingRow';
 import { fetchOPCode, getSettings } from '../services/controlHelper';
 import {
   updateScheduledNotifs,
@@ -425,6 +426,7 @@ const ProfileSettings = () => {
           desc={authSettings.opcode}
           descStyle={settingStyles.monoDesc}></SettingRow>
         <DemographicsSettingRow></DemographicsSettingRow>
+        {appConfig?.survey_info?.['trip-labels'] == 'MULTILABEL' && <CustomLabelSettingRow />}
         <SettingRow
           textKey="control.view-privacy"
           iconName="eye"
diff --git a/www/js/services/commHelper.ts b/www/js/services/commHelper.ts
index 75fdbf8de..012089313 100644
--- a/www/js/services/commHelper.ts
+++ b/www/js/services/commHelper.ts
@@ -233,3 +233,77 @@ export function putOne(key, data) {
     throw error;
   });
 }
+
+export function getUserCustomLabels(keys) {
+  return new Promise<any>((rs, rj) => {
+    window['cordova'].plugins.BEMServerComm.postUserPersonalData(
+      '/customlabel/get',
+      'keys',
+      keys,
+      rs,
+      rj,
+    );
+  }).catch((error) => {
+    error = 'While getting labels, ' + error;
+    throw error;
+  });
+}
+
+export function insertUserCustomLabel(key, newLabel) {
+  const insertedLabel = {
+    key: key,
+    label: newLabel,
+  };
+  return new Promise((rs, rj) => {
+    window['cordova'].plugins.BEMServerComm.postUserPersonalData(
+      '/customlabel/insert',
+      'inserted_label',
+      insertedLabel,
+      rs,
+      rj,
+    );
+  }).catch((error) => {
+    error = `While inserting one ${key}, ${error}`;
+    throw error;
+  });
+}
+
+export function updateUserCustomLabel(key, oldLabel, newLabel, isNewLabelMustAdded) {
+  const updatedLabel = {
+    key: key,
+    old_label: oldLabel,
+    new_label: newLabel,
+    is_new_label_must_added: isNewLabelMustAdded,
+  };
+  return new Promise<any>((rs, rj) => {
+    window['cordova'].plugins.BEMServerComm.postUserPersonalData(
+      '/customlabel/update',
+      'updated_label',
+      updatedLabel,
+      rs,
+      rj,
+    );
+  }).catch((error) => {
+    error = `While updating one ${key}, ${error}`;
+    throw error;
+  });
+}
+
+export function deleteUserCustomLabel(key, newLabel) {
+  const deletedLabel = {
+    key: key,
+    label: newLabel,
+  };
+  return new Promise((rs, rj) => {
+    window['cordova'].plugins.BEMServerComm.postUserPersonalData(
+      '/customlabel/delete',
+      'deleted_label',
+      deletedLabel,
+      rs,
+      rj,
+    );
+  }).catch((error) => {
+    error = `While deleting one ${key}, ${error}`;
+    throw error;
+  });
+}
diff --git a/www/js/survey/multilabel/MultiLabelButtonGroup.tsx b/www/js/survey/multilabel/MultiLabelButtonGroup.tsx
index 6c4c876ce..466bb9868 100644
--- a/www/js/survey/multilabel/MultiLabelButtonGroup.tsx
+++ b/www/js/survey/multilabel/MultiLabelButtonGroup.tsx
@@ -12,6 +12,7 @@ import {
   RadioButton,
   Button,
   TextInput,
+  Divider,
 } from 'react-native-paper';
 import DiaryButton from '../../components/DiaryButton';
 import { useTranslation } from 'react-i18next';
@@ -29,22 +30,23 @@ import {
 } from './confirmHelper';
 import useAppConfig from '../../useAppConfig';
 import { MultilabelKey } from '../../types/labelTypes';
+import { updateUserCustomLabel } from '../../services/commHelper';
+import { AppContext } from '../../App';
 
 const MultilabelButtonGroup = ({ trip, buttonsInline = false }) => {
   const { colors } = useTheme();
   const { t } = useTranslation();
   const appConfig = useAppConfig();
   const { labelOptions, labelFor, userInputFor, addUserInputToEntry } = useContext(TimelineContext);
+  const { customLabelMap, setCustomLabelMap } = useContext(AppContext);
   const { height: windowHeight } = useWindowDimensions();
-
   // modal visible for which input type? (MODE or PURPOSE or REPLACED_MODE, null if not visible)
   const [modalVisibleFor, setModalVisibleFor] = useState<MultilabelKey | null>(null);
   const [otherLabel, setOtherLabel] = useState<string | null>(null);
-  const chosenLabel = useMemo<string | null>(() => {
+  const initialLabel = useMemo<string | null>(() => {
     if (modalVisibleFor == null) return null;
-    if (otherLabel != null) return 'other';
     return labelFor(trip, modalVisibleFor)?.value || null;
-  }, [modalVisibleFor, otherLabel]);
+  }, [modalVisibleFor]);
 
   // to mark 'inferred' labels as 'confirmed'; turn yellow labels blue
   function verifyTrip() {
@@ -81,16 +83,36 @@ const MultilabelButtonGroup = ({ trip, buttonsInline = false }) => {
     if (!Object.keys(inputs).length) return displayErrorMsg('No inputs to store');
     const inputsToStore: UserInputMap = {};
     const storePromises: any[] = [];
-    for (let [inputType, chosenLabel] of Object.entries(inputs)) {
+
+    for (let [inputType, newLabel] of Object.entries(inputs)) {
       if (isOther) {
         /* Let's make the value for user entered inputs look consistent with our other values
         (i.e. lowercase, and with underscores instead of spaces) */
-        chosenLabel = readableLabelToKey(chosenLabel);
+        newLabel = readableLabelToKey(newLabel);
+      }
+      // If a user saves a new customized label or makes changes to/from customized labels, the labels need to be updated.
+      const key = inputType.toLowerCase();
+      if (
+        isOther ||
+        (initialLabel && customLabelMap[key].indexOf(initialLabel) > -1) ||
+        (newLabel && customLabelMap[key].indexOf(newLabel) > -1)
+      ) {
+        updateUserCustomLabel(key, initialLabel ?? '', newLabel, isOther ?? false)
+          .then((res) => {
+            setCustomLabelMap({
+              ...customLabelMap,
+              [key]: res['label'],
+            });
+            logDebug('Successfuly stored custom label ' + JSON.stringify(res));
+          })
+          .catch((e) => {
+            displayErrorMsg(e, 'Create Label Error');
+          });
       }
       const inputDataToStore = {
         start_ts: trip.start_ts,
         end_ts: trip.end_ts,
-        label: chosenLabel,
+        label: newLabel,
       };
       inputsToStore[inputType] = inputDataToStore;
 
@@ -107,6 +129,8 @@ const MultilabelButtonGroup = ({ trip, buttonsInline = false }) => {
   }
 
   const tripInputDetails = labelInputDetailsForTrip(userInputFor(trip), appConfig);
+  const customLabelKeyInDatabase = modalVisibleFor === 'PURPOSE' ? 'purpose' : 'mode';
+
   return (
     <>
       <View style={{ flexDirection: 'row', alignItems: 'center' }}>
@@ -164,16 +188,47 @@ const MultilabelButtonGroup = ({ trip, buttonsInline = false }) => {
               <ScrollView style={{ paddingBottom: 24 }}>
                 <RadioButton.Group
                   onValueChange={(val) => onChooseLabel(val)}
-                  value={chosenLabel || ''}>
+                  // if 'other' button is selected and input component shows up, make 'other' radio button filled
+                  value={otherLabel !== null ? 'other' : initialLabel || ''}>
                   {modalVisibleFor &&
-                    labelOptions?.[modalVisibleFor]?.map((o, i) => (
-                      <RadioButton.Item
-                        key={i}
-                        label={o.text || labelKeyToReadable(o.value)}
-                        value={o.value}
-                        style={{ paddingVertical: 2 }}
-                      />
-                    ))}
+                    labelOptions?.[modalVisibleFor]?.map((o, i) => {
+                      const radioItemForOption = (
+                        <RadioButton.Item
+                          key={i}
+                          label={o.text || labelKeyToReadable(o.value)}
+                          value={o.value}
+                          style={{ paddingVertical: 2 }}
+                        />
+                      );
+                      /* if this is the 'other' option and there are some custom labels,
+                      show the custom labels section before 'other' */
+                      if (o.value == 'other' && customLabelMap[customLabelKeyInDatabase]?.length) {
+                        return (
+                          <>
+                            <Divider style={{ marginVertical: 10 }} />
+                            <Text
+                              style={{ fontSize: 12, color: colors.onSurface, paddingVertical: 4 }}>
+                              {(modalVisibleFor === 'MODE' ||
+                                modalVisibleFor === 'REPLACED_MODE') &&
+                                t('trip-confirm.custom-mode')}
+                              {modalVisibleFor === 'PURPOSE' && t('trip-confirm.custom-purpose')}
+                            </Text>
+                            {customLabelMap[customLabelKeyInDatabase].map((key, i) => (
+                              <RadioButton.Item
+                                key={i}
+                                label={labelKeyToReadable(key)}
+                                value={key}
+                                style={{ paddingVertical: 2 }}
+                              />
+                            ))}
+                            <Divider style={{ marginVertical: 10 }} />
+                            {radioItemForOption}
+                          </>
+                        );
+                      }
+                      // otherwise, just show the radio item as normal
+                      return radioItemForOption;
+                    })}
                 </RadioButton.Group>
               </ScrollView>
             </Dialog.Content>
@@ -185,6 +240,7 @@ const MultilabelButtonGroup = ({ trip, buttonsInline = false }) => {
                   })}
                   value={otherLabel || ''}
                   onChangeText={(t) => setOtherLabel(t)}
+                  maxLength={25}
                 />
                 <Dialog.Actions>
                   <Button onPress={() => store({ [modalVisibleFor]: otherLabel }, true)}>