diff --git a/tag.go b/tag.go
index d7c9750..d188f09 100644
--- a/tag.go
+++ b/tag.go
@@ -51,6 +51,11 @@ func (tag *Tag) AddAttachedPicture(pf PictureFrame) {
 	tag.AddFrame(tag.CommonID("Attached picture"), pf)
 }
 
+// AddChapterFrame adds the chapter frame to tag.
+func (tag *Tag) AddChapterFrame(cf ChapterFrame) {
+	tag.AddFrame(tag.CommonID("Chapters"), cf)
+}
+
 // AddCommentFrame adds the comment frame to tag.
 func (tag *Tag) AddCommentFrame(cf CommentFrame) {
 	tag.AddFrame(tag.CommonID("Comments"), cf)
diff --git a/v2/chapter_toc_frame.go b/v2/chapter_toc_frame.go
new file mode 100644
index 0000000..8ee2920
--- /dev/null
+++ b/v2/chapter_toc_frame.go
@@ -0,0 +1,141 @@
+package id3v2
+
+import (
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"io"
+)
+
+const (
+	maskOrdered  = byte(1 << 0)
+	maskToplevel = byte(1 << 1)
+)
+
+var ErrUnexpectedId = errors.New("unexpected ID")
+
+type ChapterTocFrame struct {
+	ElementID string
+	// This frame is the root of the Table of Contents tree and is not a child of any other "CTOC" frame.
+	TopLevel bool
+	// This provides a hint as to whether the elements should be played as a continuous ordered sequence or played individually.
+	Ordered     bool
+	ChapterIds  []string
+	Description *TextFrame
+}
+
+func (ctf ChapterTocFrame) Size() int {
+	size := encodedSize(ctf.ElementID, EncodingISO)
+	size += 1 // trailing zero after ElementID
+	size += 1 // CTOC Flags
+	// The Entry count is the number of entries in the Child Element ID
+	// list that follows and must be greater than zero.
+	size += 1 // Entrycount
+
+	// entries
+	for _, id := range ctf.ChapterIds {
+		size += encodedSize(id, EncodingISO)
+		size += 1 // trailing zero after ID
+	}
+
+	// (optional) descriptive data
+	if ctf.Description != nil {
+		size += frameHeaderSize // Description frame header size
+		size += ctf.Description.Size()
+	}
+
+	return size
+}
+
+func (ctf ChapterTocFrame) UniqueIdentifier() string {
+	return ctf.ElementID
+}
+
+func (ctf ChapterTocFrame) WriteTo(w io.Writer) (n int64, err error) {
+	return useBufWriter(w, func(bw *bufWriter) {
+		bw.EncodeAndWriteText(ctf.ElementID, EncodingISO)
+		bw.WriteByte(0)
+
+		ctocFlags := byte(0)
+		if ctf.TopLevel {
+			ctocFlags |= maskToplevel
+		}
+		if ctf.Ordered {
+			ctocFlags |= maskOrdered
+		}
+
+		binary.Write(bw, binary.BigEndian, ctocFlags)
+
+		binary.Write(bw, binary.BigEndian, uint8(len(ctf.ChapterIds)))
+
+		for _, id := range ctf.ChapterIds {
+			bw.EncodeAndWriteText(id, EncodingISO)
+			bw.WriteByte(0)
+		}
+
+		if ctf.Description != nil {
+			writeFrame(bw, "TIT2", *ctf.Description, true)
+		}
+	})
+}
+
+func parseChapterTocFrame(br *bufReader, version byte) (Framer, error) {
+	elementID := string(br.ReadText(EncodingISO))
+	synchSafe := version == 4
+	var ctocFlags byte
+	if err := binary.Read(br, binary.BigEndian, &ctocFlags); err != nil {
+		return nil, err
+	}
+
+	var elements uint8
+	if err := binary.Read(br, binary.BigEndian, &elements); err != nil {
+		return nil, err
+	}
+
+	chaptersIDs := make([]string, elements)
+	for i := uint8(0); i < elements; i++ {
+		chaptersIDs[i] = string(br.ReadText(EncodingISO))
+	}
+
+	var description TextFrame
+
+	// borrowed from parse.go
+	buf := getByteSlice(32 * 1024)
+	defer putByteSlice(buf)
+
+	for {
+		header, err := parseFrameHeader(buf, br, synchSafe)
+		if err == io.EOF || err == errBlankFrame || err == ErrInvalidSizeFormat {
+			break
+		}
+
+		if err != nil {
+			return nil, err
+		}
+
+		if header.ID != "TIT2" {
+			return nil, fmt.Errorf("expected: '%s', got: '%s'  : %w", "TIT2", header.ID, ErrUnexpectedId)
+		}
+
+		bodyRd := getLimitedReader(br, header.BodySize)
+		br := newBufReader(bodyRd)
+		frame, err := parseTextFrame(br)
+		if err != nil {
+			putLimitedReader(bodyRd)
+			return nil, err
+		}
+		description = frame.(TextFrame)
+
+		putLimitedReader(bodyRd)
+	}
+
+	tocFrame := ChapterTocFrame{
+		ElementID:   elementID,
+		TopLevel:    (ctocFlags & maskToplevel) == maskToplevel,
+		Ordered:     (ctocFlags & maskOrdered) == maskOrdered,
+		ChapterIds:  chaptersIDs,
+		Description: &description,
+	}
+
+	return tocFrame, nil
+}
diff --git a/v2/chapter_toc_frame_test.go b/v2/chapter_toc_frame_test.go
new file mode 100644
index 0000000..d17da28
--- /dev/null
+++ b/v2/chapter_toc_frame_test.go
@@ -0,0 +1,116 @@
+package id3v2
+
+import (
+	"bytes"
+	"fmt"
+	"log"
+	"testing"
+	"time"
+)
+
+const (
+	testChapterTocSampleTitle = "Chapter TOC title"
+)
+
+func newChapterFrames(noOfChapters int) []ChapterFrame {
+	var start time.Duration
+	offset := time.Duration(1000 * nanosInMillis)
+
+	chapters := make([]ChapterFrame, noOfChapters)
+
+	for i := 0; i < noOfChapters; i++ {
+		end := start + offset
+
+		chapters[i] = ChapterFrame{
+			ElementID:   fmt.Sprintf("ch%d", i),
+			StartTime:   start,
+			EndTime:     end,
+			StartOffset: IgnoredOffset,
+			EndOffset:   IgnoredOffset,
+			Title: &TextFrame{
+				Encoding: EncodingUTF8,
+				Text:     fmt.Sprintf("Chapter %d", i),
+			},
+		}
+
+		start = end
+	}
+
+	return chapters
+}
+
+func TestAddChapterTocFrame(t *testing.T) {
+	const noOfChapters = 5
+	buf := &bytes.Buffer{}
+	tag := NewEmptyTag()
+
+	chapters := newChapterFrames(noOfChapters)
+
+	chapterIds := make([]string, len(chapters))
+	for i, c := range chapters {
+		tag.AddChapterFrame(c)
+
+		chapterIds[i] = c.ElementID
+	}
+
+	chapterToc := ChapterTocFrame{
+		ElementID:  "Main TOC",
+		TopLevel:   true,
+		Ordered:    true,
+		ChapterIds: chapterIds,
+		Description: &TextFrame{
+			Encoding: EncodingUTF8,
+			Text:     testChapterTocSampleTitle,
+		},
+	}
+
+	tag.AddChapterTocFrame(chapterToc)
+	tag.WriteTo(buf)
+
+	// Read back
+
+	tagBack, err := ParseReader(buf, Options{Parse: true})
+	if err != nil {
+		log.Fatal("Error parsing mp3 content: ", err)
+	}
+
+	if !tagBack.HasFrames() {
+		log.Fatal("No tags in content in mp3 content")
+	}
+
+	chapterTocBackFrame := tag.GetLastFrame("CTOC")
+	if chapterTocBackFrame == nil {
+		log.Fatal("Error getting chapter TOC frame: ", err)
+	}
+
+	chapterTocBack, ok := chapterTocBackFrame.(ChapterTocFrame)
+	if !ok {
+		log.Fatal("Error casting chapter TOC frame")
+	}
+
+	if chapterToc.ElementID != chapterTocBack.ElementID {
+		t.Errorf("Expected element ID: %s, but got %s", chapterToc.ElementID, chapterTocBack.ElementID)
+	}
+
+	if chapterToc.TopLevel != chapterTocBack.TopLevel {
+		t.Errorf("Expected top level: %v, but got %v", chapterToc.TopLevel, chapterTocBack.TopLevel)
+	}
+
+	if chapterToc.Ordered != chapterTocBack.Ordered {
+		t.Errorf("Expected ordered: %v, but got %v", chapterToc.Ordered, chapterTocBack.Ordered)
+	}
+
+	if expected, actual := len(chapterToc.ChapterIds), len(chapterTocBack.ChapterIds); expected != actual {
+		t.Errorf("Expected ordered: %v, but got %v", expected, actual)
+	}
+
+	for i := 0; i < len(chapterToc.ChapterIds); i++ {
+		if expected, actual := chapterToc.ChapterIds[i], chapterTocBack.ChapterIds[i]; expected != actual {
+			t.Errorf("Expected chapter reference at index: %d: %s, but got %s", i, expected, actual)
+		}
+	}
+
+	if chapterToc.Description != nil && chapterToc.Description.Text != chapterTocBack.Description.Text {
+		t.Errorf("Expected description: %s, but got %s", chapterToc.Description.Text, chapterTocBack.Description.Text)
+	}
+}
diff --git a/v2/common_ids.go b/v2/common_ids.go
index aeac14d..30e35c1 100644
--- a/v2/common_ids.go
+++ b/v2/common_ids.go
@@ -11,6 +11,7 @@ var (
 	V23CommonIDs = map[string]string{
 		"Attached picture":                   "APIC",
 		"Chapters":                           "CHAP",
+		"Chapters TOC":                       "CTOC",
 		"Comments":                           "COMM",
 		"Album/Movie/Show title":             "TALB",
 		"BPM":                                "TBPM",
@@ -64,6 +65,7 @@ var (
 	V24CommonIDs = map[string]string{
 		"Attached picture":                   "APIC",
 		"Chapters":                           "CHAP",
+		"Chapters TOC":                       "CTOC",
 		"Comments":                           "COMM",
 		"Album/Movie/Show title":             "TALB",
 		"BPM":                                "TBPM",
@@ -140,6 +142,7 @@ var (
 var parsers = map[string]func(*bufReader, byte) (Framer, error){
 	"APIC": parsePictureFrame,
 	"CHAP": parseChapterFrame,
+	"CTOC": parseChapterTocFrame,
 	"COMM": parseCommentFrame,
 	"POPM": parsePopularimeterFrame,
 	"TXXX": parseUserDefinedTextFrame,
diff --git a/v2/tag.go b/v2/tag.go
index d188f09..ed6042c 100644
--- a/v2/tag.go
+++ b/v2/tag.go
@@ -56,6 +56,10 @@ func (tag *Tag) AddChapterFrame(cf ChapterFrame) {
 	tag.AddFrame(tag.CommonID("Chapters"), cf)
 }
 
+func (tag *Tag) AddChapterTocFrame(ctf ChapterTocFrame) {
+	tag.AddFrame(tag.CommonID("Chapters TOC"), ctf)
+}
+
 // AddCommentFrame adds the comment frame to tag.
 func (tag *Tag) AddCommentFrame(cf CommentFrame) {
 	tag.AddFrame(tag.CommonID("Comments"), cf)